/**
 * Copyright (c) 2012-2013 Reficio (TM) - Reestablish your software!. All Rights Reserved.
 *
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.reficio.ws.common;

/**
 * User: Tom Bujok (tom.bujok@gmail.com)
 * Date: 14/10/11
 * Time: 10:31 AM
 */

import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import java.io.ByteArrayInputStream;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

public class XmlComparator {

    private boolean nodeTypeDiff = true;
    private boolean nodeValueDiff = true;

    public boolean diff(String xml1, String xml2, List<String> diffs) throws Exception {
        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
        dbf.setNamespaceAware(true);
        dbf.setCoalescing(true);
        dbf.setIgnoringElementContentWhitespace(true);
        dbf.setIgnoringComments(true);
        DocumentBuilder db = dbf.newDocumentBuilder();


        Document doc1 = db.parse(new ByteArrayInputStream(xml1.getBytes()));
        Document doc2 = db.parse(new ByteArrayInputStream(xml2.getBytes()));

        doc1.normalizeDocument();
        doc2.normalizeDocument();

        // log.info("\n" + XmlUtils.serializePretty(doc1));
        // log.info("\n" + XmlUtils.serializePretty(doc2));

        return diff(doc1, doc2, diffs);

    }

    /**
     * Diff 2 nodes and put the diffs in the list
     */
    public boolean diff(Node node1, Node node2, List<String> diffs) throws Exception {
        if (diffNodeExists(node1, node2, diffs)) {
            return true;
        }

        if (nodeTypeDiff) {
            diffNodeType(node1, node2, diffs);
        }

        if (nodeValueDiff) {
            diffNodeValue(node1, node2, diffs);
        }

//        log.info(node1.getNodeName() + "/" + node2.getNodeName());

        diffAttributes(node1, node2, diffs);
        diffNodes(node1, node2, diffs);

        return diffs.size() > 0;
    }

    /**
     * Diff the nodes
     */
    public boolean diffNodes(Node node1, Node node2, List<String> diffs) throws Exception {
        //Sort by Name
        Map<String, Node> children1 = new LinkedHashMap<String, Node>();
        for (Node child1 = node1.getFirstChild(); child1 != null; child1 = child1.getNextSibling()) {
            children1.put(child1.getNodeName(), child1);
        }

        //Sort by Name
        Map<String, Node> children2 = new LinkedHashMap<String, Node>();
        for (Node child2 = node1.getFirstChild(); child2 != null; child2 = child2.getNextSibling()) {
            children2.put(child2.getNodeName(), child2);
        }

        //Diff all the children1
        for (Node child1 : children1.values()) {
            Node child2 = children2.remove(child1.getNodeName());
            diff(child1, child2, diffs);
        }

        //Diff all the children2 left over
        for (Node child2 : children2.values()) {
            Node child1 = children1.get(child2.getNodeName());
            diff(child1, child2, diffs);
        }

        return diffs.size() > 0;
    }


    /**
     * Diff the nodes
     */
    public boolean diffAttributes(Node node1, Node node2, List<String> diffs) throws Exception {
        //Sort by Name
        NamedNodeMap nodeMap1 = node1.getAttributes();
        Map<String, Node> attributes1 = new LinkedHashMap<String, Node>();
        for (int index = 0; nodeMap1 != null && index < nodeMap1.getLength(); index++) {
            attributes1.put(nodeMap1.item(index).getNodeName(), nodeMap1.item(index));
        }

        //Sort by Name
        NamedNodeMap nodeMap2 = node2.getAttributes();
        Map<String, Node> attributes2 = new LinkedHashMap<String, Node>();
        for (int index = 0; nodeMap2 != null && index < nodeMap2.getLength(); index++) {
            attributes2.put(nodeMap2.item(index).getNodeName(), nodeMap2.item(index));

        }

        //Diff all the attributes1
        for (Node attribute1 : attributes1.values()) {
            Node attribute2 = attributes2.remove(attribute1.getNodeName());
            diff(attribute1, attribute2, diffs);
        }

        //Diff all the attributes2 left over
        for (Node attribute2 : attributes2.values()) {
            Node attribute1 = attributes1.get(attribute2.getNodeName());
            diff(attribute1, attribute2, diffs);
        }

        return diffs.size() > 0;
    }

    /**
     * Check that the nodes exist
     */
    public boolean diffNodeExists(Node node1, Node node2, List<String> diffs) throws Exception {
        if (node1 == null && node2 == null) {
            diffs.add(getPath(node2) + ":node " + node1 + "!=" + node2 + "\n");
            return true;
        }

        if (node1 == null && node2 != null) {
            diffs.add(getPath(node2) + ":node " + node1 + "!=" + node2.getNodeName());
            return true;
        }

        if (node1 != null && node2 == null) {
            diffs.add(getPath(node1) + ":node " + node1.getNodeName() + "!=" + node2);
            return true;
        }

        return false;
    }

    /**
     * Diff the Node Type
     */
    public boolean diffNodeType(Node node1, Node node2, List<String> diffs) throws Exception {
        if (node1.getNodeType() != node2.getNodeType()) {
            diffs.add(getPath(node1) + ":type " + node1.getNodeType() + "!=" + node2.getNodeType());
            return true;
        }

        return false;
    }

    /**
     * Diff the Node Value
     */
    public boolean diffNodeValue(Node node1, Node node2, List<String> diffs) throws Exception {
        if (node1.getNodeValue() == null && node2.getNodeValue() == null) {
            return false;
        }

        if (node1.getNodeValue() == null && node2.getNodeValue() != null) {
            diffs.add(getPath(node1) + ":type " + node1 + "!=" + node2.getNodeValue());
            return true;
        }

        if (node1.getNodeValue() != null && node2.getNodeValue() == null) {
            diffs.add(getPath(node1) + ":type " + node1.getNodeValue() + "!=" + node2);
            return true;
        }

        if (!node1.getNodeValue().equals(node2.getNodeValue())) {
            diffs.add(getPath(node1) + ":type " + node1.getNodeValue() + "!=" + node2.getNodeValue());
            return true;
        }

        return false;
    }


    /**
     * Get the node path
     */
    public String getPath(Node node) {
        StringBuilder path = new StringBuilder();

        do {
            path.insert(0, node.getNodeName());
            path.insert(0, "/");
        }
        while ((node = node.getParentNode()) != null);

        return path.toString();
    }
}
